/* =================================================================
 * This file is part of oBoxKit.
 *
 * oBoxKit is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * oBoxKit is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with oBoxKit.  If not, see <http://www.gnu.org/licenses/>.
 * ================================================================== */
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <locale.h>
#include <signal.h>
#include <assert.h>
#include <stdarg.h>
#include <malloc.h>
#include <pulse/pulseaudio.h>


#define WRITE_MSG(msg) { \
    printf msg; \
    printf ("\n"); \
}

#define PAVOL_ADJUST_ALL -1
#define PAVOL_APP_NAME "oPAVol"

#define PAVOL_DBG_ENVVAR "PAVOL_DBG"
#define DBGPRINT(msg) {\
    dbg_printf msg; \
}

#define IF_FAIL(cond, jumpto, msg) { \
    if (!(cond)) { \
        printf msg; \
        printf("\n"); \
        goto jumpto; \
    } \
}

#ifndef _
#define _(str) str
#endif

#define NO_ERROR 0
#define ERROR_RET 1

typedef enum work_mode_t
{
    WORKMODE_MUTE,
    WORKMODE_UNMUTE,
    WORKMODE_TOGGLE,
    WORKMODE_SETVOL
} work_mode_t;


typedef struct volume_info_t
{
    int index;
    int mute;
    pa_cvolume volume;
    uint32_t n_volume_steps;
} volume_info_t;

typedef struct args_def_t
{
    int sink_id;
    work_mode_t mode;
    int delta_percent;
    volume_info_t volinfo;
    pa_mainloop_api* api;
} args_def_t;

static void dbg_printf(const char* format, ...)
{
    va_list ap;
    char* dbgenv = NULL;
    dbgenv = getenv(PAVOL_DBG_ENVVAR);
    if (dbgenv != NULL) {
        va_start(ap, format);
        vfprintf(stderr, format, ap);
        fprintf(stderr, "\n");
        va_end(ap);
    }
    return;
}

static void usage()
{
    WRITE_MSG((_("%s: A simple PulseAudio volume adjusting tool."),
                PAVOL_APP_NAME));
    WRITE_MSG((_("Usage:\n\t%s <mute|unmute|toggle|+n|-n> [sink id]"),
                PAVOL_APP_NAME));
    WRITE_MSG((_("\tIf ID is omitted, all channels will be adjusted.")));
    return;
}

static int
parse_args(int argc, char* argv[], args_def_t* argsdef)
{
    int retval = NO_ERROR;
    int sym = 0;
    char* p = NULL;
    if (argc != 2 && argc != 3) {
        retval = (argc == 1? NO_ERROR: ERROR_RET);
        usage();
        goto errret;
    }
    if (0 == strcasecmp(argv[1], "mute")) {
        argsdef->mode = WORKMODE_MUTE;
    } else if (0 == strcasecmp(argv[1], "unmute")) {
        argsdef->mode = WORKMODE_UNMUTE;
    } else if (0 == strcasecmp(argv[1], "toggle")) {
        argsdef->mode = WORKMODE_TOGGLE;
    } else {
        if (argv[1][0] == '-')
            sym = -1;
        else if (argv[1][0] == '+')
            sym = 1;
        else {
            retval = ERROR_RET;
            usage();
            goto errret;
        }
        // Make sure only full digital strings can be parsed.
        for (p = &(argv[1][1]); (*p) != '\0'; ++p) {
            DBGPRINT(("char = %c", *p));
            if(!isdigit((*p))) {
                retval = ERROR_RET;
                usage();
                goto errret;
            }
        }
        argsdef->delta_percent = sym * atoi(&(argv[1][1]));
        argsdef->mode  = WORKMODE_SETVOL;
    }
    if (argc == 3) {
        for (p = &(argv[2][0]); (*p) != '\0'; ++p) {
            if(!isdigit((*p))) {
                retval = ERROR_RET;
                usage();
                goto errret;
            }
        }
        argsdef->sink_id = atoi(argv[2]);
    } else {
        argsdef->sink_id = PAVOL_ADJUST_ALL;
    }
    DBGPRINT(("Specify sink: %d", argsdef->sink_id));
errret:
    return retval;
}

static void quit_loop(pa_mainloop_api* api, int retval)
{
    DBGPRINT(("DONE"));
    api->quit(api, retval);
}

static void exit_signal_cb(pa_mainloop_api* api, pa_signal_event* event,
                           int sig, void* data)
{
    (void)data;
    (void)sig;
    (void)event;
    DBGPRINT(("Receive SIGTERM or SIGINT, quit."));
    quit_loop(api, NO_ERROR);
}


static void
complete_cb(pa_context* context, int success, void* data)
{
    args_def_t* argsdef_copy = (args_def_t*)data;
    if (!success)
        WRITE_MSG((_("Failure: %s"),
                    pa_strerror(pa_context_errno(context))));
    // Exit.
    quit_loop(argsdef_copy->api, success);
    // Now we can safely delete argsdef_copy
    free(argsdef_copy);
    return;
}

static void set_volume_state(pa_context* context, args_def_t* argsdef_copy)
{
    pa_operation* op = NULL;
    pa_cvolume newvol;
    work_mode_t mode;
    int delta_percent = 0;
    volume_info_t* volinfo = NULL;
    int i = 0;

    mode = argsdef_copy->mode;
    volinfo = &(argsdef_copy->volinfo);
    delta_percent = argsdef_copy->delta_percent;
    DBGPRINT(("Args mode: 0x%x", mode));
    double maxval = 0.0;
    double newval = 0.0;
    double currval = 0.0;
    // Get active sink

    switch(mode)
    {
        case WORKMODE_MUTE:
            op = pa_context_set_sink_mute_by_index(context,
                    volinfo->index, 1, complete_cb, argsdef_copy);
            pa_operation_unref(op);
            break;
        case WORKMODE_UNMUTE:
            op = pa_context_set_sink_mute_by_index(context,
                    volinfo->index, 0, complete_cb, argsdef_copy);
            pa_operation_unref(op);
            break;
        case WORKMODE_TOGGLE:
            op = pa_context_set_sink_mute_by_index(context,
                    volinfo->index, (volinfo->mute? 0: 1),
                    complete_cb, argsdef_copy);
            pa_operation_unref(op);
            break;
        case WORKMODE_SETVOL:
            newvol.channels = volinfo->volume.channels;
            for (i = 0; i< volinfo->volume.channels; ++i) {
                maxval = volinfo->n_volume_steps;
                currval = volinfo->volume.values[i];
                newval = currval + maxval / 100 * delta_percent;
                if (newval < 0) {
                    newvol.values[i] = 0;
                } else if (newval >= maxval) {
                    newvol.values[i] = volinfo->n_volume_steps;
                } else {
                    newvol.values[i] = (pa_volume_t)newval;
                }
                DBGPRINT(("Channel %d: set volume from %d to %d", i,
                            volinfo->volume.values[i], newvol.values[i]));
                // XXX Analog device may have different channel values,
                // and the difference may increase after calculation. 
                // So we always adjust all channels to the same value.
                // NOTE: This is very hard to repro so I don't know if
                // this is a bug or just the behavior.
                if (newvol.values[i] != newvol.values[0]) {
                    DBGPRINT(("Possible analog device"));
                    DBGPRINT(("Different volume with channel 0: %d",
                                newvol.values[i]));
                    newvol.values[i] = newvol.values[0];
                    DBGPRINT(("After tunning: volume from %d to %d", i,
                                volinfo->volume.values[i],
                                newvol.values[i]));
                }
            }
            op = pa_context_set_sink_volume_by_index(context,
                    volinfo->index, &newvol, complete_cb, argsdef_copy);
            pa_operation_unref(op);
            break;
        default:
            break;
    }
    return;
}

static void drain_cb(pa_context* context, void* data)
{
    pa_context_disconnect(context);
}

static void mark_complete(pa_context* context)
{
    pa_operation* op = NULL;
    op = pa_context_drain(context, drain_cb, NULL);
    if (op)
        pa_operation_unref(op);
    else
        pa_context_disconnect(context);
}

static void
get_sink_info_cb(pa_context* context, const pa_sink_info* info,
                 int is_last, void* data)
{
    args_def_t* argsdef = (args_def_t*)data;
    args_def_t* argsdef_copy = NULL;
    DBGPRINT(("Entering get_sink_info_cb()"));
    if (is_last < 0) {
        WRITE_MSG((_("Failed to get sink (audio output) information: %s"),
                    pa_strerror(pa_context_errno(context))));
        goto errret;
    }
    if (is_last) {
        DBGPRINT(("All sinks checked, status = %s",
                    pa_strerror(pa_context_errno(context))));
        mark_complete(context);
        goto errret;
    }
    // We got a new sink. Allocate a buffer to store the information.
    // NOTE: The buffer must be freed in complete_cb();.
    DBGPRINT(("Got new sink: index = %d", info->index));
    if (argsdef->sink_id == PAVOL_ADJUST_ALL ||
            argsdef->sink_id == info->index)
    {
        if (argsdef->sink_id == PAVOL_ADJUST_ALL) {
            DBGPRINT(("Update all sinks"));
        } else {
            DBGPRINT(("Update specified volume state on: %d -- %s", 
                        info->index, info->name));
        }
        argsdef_copy = (args_def_t*)malloc(sizeof(args_def_t));
        if (!argsdef_copy) {
            WRITE_MSG((_("Error: Failed to malloc() on sink %s"),
                        info->name));
            goto errret;
        }
        DBGPRINT(("Update volume state on: %s", info->name));
        memcpy(argsdef_copy, argsdef, sizeof(args_def_t));
        argsdef_copy->volinfo.index = info->index;
        argsdef_copy->volinfo.mute = info->mute;
        argsdef_copy->volinfo.volume = info->volume;
        argsdef_copy->volinfo.n_volume_steps = info->n_volume_steps;
        set_volume_state(context, argsdef_copy);

    } else {
        DBGPRINT(("Got sink but no need to adjust: %d -- %s",
                    info->index, info->name));
    }
errret:
    DBGPRINT(("Leaving get_sink_info_cb()"));
    return;
}

static void find_active_sink(pa_context* context, args_def_t* argsdef)
{
    pa_operation* op = NULL;

    DBGPRINT(("Entering find_active_sink()"));
    DBGPRINT(("Context: server = %s", pa_context_get_server(context)));
    op = pa_context_get_sink_info_list(context, get_sink_info_cb, argsdef);
    pa_operation_unref(op);
    DBGPRINT(("Done for get_sink_info_list()"));
    return;
}

static void context_state_cb(pa_context* context, void* data)
{
    int context_state = 0;
    args_def_t* argsdef = (args_def_t*)data;
    context_state = pa_context_get_state(context);
    DBGPRINT(("Context state: 0x%x", context_state));
    switch(context_state)
    {
        case PA_CONTEXT_CONNECTING:
        case PA_CONTEXT_AUTHORIZING:
        case PA_CONTEXT_SETTING_NAME:
            break;
        case PA_CONTEXT_UNCONNECTED: // FIXME: Not sure if this is correct
            quit_loop(argsdef->api, NO_ERROR);
            break;
        case PA_CONTEXT_READY:
            // Do our business.
            DBGPRINT(("Context ready, call find_active_sink."));
            find_active_sink(context, argsdef);
            break;
        case PA_CONTEXT_TERMINATED:
            quit_loop(argsdef->api, ERROR_RET);
            break;
        case PA_CONTEXT_FAILED:
            quit_loop(argsdef->api, ERROR_RET);
            break;
        default:
            break;
    }
    return;
}

int main(int argc, char* argv[])
{
    int ret = NO_ERROR;
    pa_context* context = NULL;
    pa_proplist* proplist = NULL;
    pa_mainloop* mainloop = NULL;
    pa_mainloop_api* api = NULL;
    args_def_t argsdef;

    setlocale(LC_ALL, "");
    // bindtextdomain(GETTEXT_PACKAGE, PAVOL_LOCALEDIR);

    ret = parse_args(argc, argv, &argsdef);
    IF_FAIL(ret == NO_ERROR, errret, (_("Error on parse argments")));

    proplist = pa_proplist_new();
    mainloop = pa_mainloop_new();
    IF_FAIL(mainloop, errret, (_("Failed on pa_main_loop_new()")));

    // Set application name:
    pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, PAVOL_APP_NAME);

    api = pa_mainloop_get_api(mainloop);
    IF_FAIL(0 == pa_signal_init(api), errret,
            (_("Failed on pa_signal_init()")));
    pa_signal_new(SIGINT, exit_signal_cb, NULL);
    pa_signal_new(SIGTERM, exit_signal_cb, NULL);

    context = pa_context_new_with_proplist(api, NULL, proplist);
    IF_FAIL(context, errret, (_("Failed on creating context")));

    argsdef.api = api;
    pa_context_set_state_callback(context, context_state_cb, &argsdef);
    // FIXME: Second parameter: Use NULL to specify curent active server.
    DBGPRINT(("Connecting..."));
    if (pa_context_connect(context, NULL, 0, NULL) < 0)
        IF_FAIL(0, errret, (_("Fail on connect server"))); 

    if (pa_mainloop_run(mainloop, &ret) < 0)
        IF_FAIL(0, errret, (_("Fail on starting main loop."))); 
    DBGPRINT(("Done main loop"));
errret:
    if (context)
        pa_context_unref(context);
    if (mainloop)
        pa_mainloop_free(mainloop);
    if (proplist)
        pa_proplist_free(proplist);
    return ret;
}
